import re

from django.contrib.auth.models    import AnonymousUser
from django.core.exceptions        import ValidationError,ObjectDoesNotExist
from django.core.urlresolvers      import get_callable
from django.db                     import models,IntegrityError
from django.db.models              import signals,Q
from django.utils.datastructures   import SortedDict
from django.utils.translation      import ugettext_lazy as _
from sitepal.sitebase              import fields
from sitepal.sitebase.conf         import properties
from sitepal.sitebase.managers     import ResourceManager
from sitepal.sitebase.models       import Resource
from sitepal.sitemenu.models.flags import MENU_LINKS_TO_PARENT
from sitepal.sitemenu.models.menus import Menu
from sitepal.sitemenu.models.util  import get_view_from_name,get_view_from_path
from sitepal.sitemenu.models.util  import get_from_cache_or_load,filter_menu_tree_links
from sitepal.sitemenu.validators   import validate_path,validate_view_args,PathValidator

class MenuViewManager(ResourceManager):
    _cache_threshold = 512
    _path_cache      = SortedDict()
    _name_cache      = SortedDict()
    _link_tree_order = ('p0','p1','p2','p3','p4','p5','p6','p7','p8','p9','weight','title')

    @classmethod
    def _update_cache(cls,sender,instance,created,**kwargs):
        if not created:
            for path in cls._path_cache.iterkeys():
                if cls._path_cache[path].pk == instance.pk:
                    cls._path_cache[path] = instance
            if instance.name and (instance.name in cls._name_cache or \
                                  len(cls._name_cache) < cls._cache_threshold):
                cls._name_cache[instance.name] = instance
            elif not instance.name:
                items_list = filter(lambda x: x[1].pk == instance.pk,cls._name_cache.iteritems())
                for name,view in items_list:
                    del cls._name_cache[name]
        elif instance.name and len(cls._name_cache) < cls._cache_threshold:
            cls._name_cache[instance.name] = instance

    def resolve_name(self,name):
        return get_from_cache_or_load(name,self._name_cache,get_view_from_name,
                                      self._cache_threshold)
        
    def resolve_path(self,path):
        view = get_from_cache_or_load(path,self._path_cache,get_view_from_path,
                                      self._cache_threshold)
        if view:
            match = view.path_regex.match(path)
            if match:
                kwargs = match.groupdict()
                if kwargs:
                    args = ()
                else:
                    args = match.groups()
            return view,args,kwargs
        return None

    def is_home_page(self,path):
        assert isinstance(path,basestring)
        if path.startswith('/'):
            path = path[1:]
        if path.endswith('/'):
            path = path[:-1]
        return path == properties.get('menu-views-home-page')

    def _get_all_parent_ids(self,menu,path):
        from sitepal.sitemenu.models.menulinks import MenuLink

        col_names = ('p0','p1','p2','p3','p4','p5','p6','p7','p8','p9')
        rows      = MenuLink.objects.filter(menu=menu,path=path).values_list(*col_names)
        if rows:
            id_set = set()
            for cols in rows:
                id_set.update(cols)
            if 0 in id_set:
                id_set.remove(0)
            return tuple(id_set)
        return ()

    def get_view_for_path(self,path,user=None):
        user = user or AnonymousUser()
        view = self.resolve_path(path)
        return view[0] if view and user.has_perm('sitemenu.call_menuview',view[0]) else None
        
    def _for_path_wrapper(self,path,user,menu_name,wrapped):
        user = user or AnonymousUser()
        try:
            menu = Menu.objects.get_by_name(menu_name)
            if not user.has_perm('sitemenu.read_menu',menu):
                return []
        except ObjectDoesNotExist:
            return []
        return wrapped(path,user,menu,menu.name,self.get_view_for_path(path,user))

    def get_links_for_path(self,path,user=None,menu_name=None):
        def inner_helper(path,user,menu,menu_name,view):
            from sitepal.sitemenu.models.menulinks import MenuLink
            
            if view:
                parent_ids = self._get_all_parent_ids(menu,path)
                if not parent_ids:
                    parent_ids = self._get_all_parent_ids(menu,view.task_root)
                if MenuLink.objects.has_expanded(menu_name):
                    query_ids = list(parent_ids)
                    while True:
                        link_ids = MenuLink.objects.filter(Q(menu=menu) &
                                                           Q(expanded=True) &
                                                           Q(has_children=True) &
                                                           Q(parent__pk__in=query_ids) &
                                                          ~Q(pk__in=query_ids)
                                                  ).values_list('id',flat=True)
                        if not link_ids:
                            break
                        query_ids.extend(link_ids)
                else:
                    query_ids = parent_ids
                links = list(MenuLink.objects.filter(Q(menu=menu) &
                                                    (Q(parent__pk__in=query_ids) |
                                                     Q(parent__isnull=True))
                                            ).order_by(*self._link_tree_order))
            else:
                links = list(MenuLink.objects.filter(menu=menu,parent__isnull=True
                                            ).order_by(*self._link_tree_order))
            return filter_menu_tree_links(links,user)

        return self._for_path_wrapper(path,user,menu_name,inner_helper)

    def get_children_links_for_path(self,path,user=None,menu_name=None):
        def inner_helper(path,user,menu,menu_name,view):
            from sitepal.sitemenu.models.menulinks import MenuLink
            try:
                links = MenuLink.objects.get(menu=menu,path__exact=path).children
                return filter_menu_tree_links(links,user)
            except ObjectDoesNotExist:
                return ()

        return self._for_path_wrapper(path,user,menu_name,inner_helper)

    def get_tasks_for_path(self,path,user=None,menu_name=None):
        def inner_helper(path,user,menu,menu_name,view):
            from sitepal.sitemenu.models.menulinks import MenuLink
            
            if view:
                if view.type_flags & MENU_LINKS_TO_PARENT:
                    view = self.get_view_from_path(view.task_parent)
                if not view:
                    return []
                tasks = list(self.filter(task_root=view.task_root,task_parent=view.task_root
                                ).order_by('weight','title'))
                tasks.sort(key=lambda x: (bool(x.type_flags & MENU_LINKS_TO_PARENT),x.weight,x.title),
                           reverse=True)
                return filter(lambda x: user.has_perm('sitemenu.call_menuview',x),tasks)
            return []

        return self._for_path_wrapper(path,user,menu_name,inner_helper)

class MenuView(Resource):
    path        = models.CharField(_("path"),max_length=255,validators=[validate_path])
    title       = models.CharField(_("title"),max_length=100)
    description = models.TextField(_("description"),max_length=8192,null=True,blank=True)
    menu        = models.ForeignKey(Menu,verbose_name=_("menu"),editable=False)
    view_func   = models.CharField(_("view function"),max_length=255,null=True,blank=True)
    view_args   = fields.JSONListField(_("view arguments"),max_length=1024,null=True,blank=True,
                                       validators=[validate_view_args])
    parts_fit   = models.IntegerField(_("fit"),editable=False,default=0,db_index=True)
    parts_count = models.IntegerField(_("count"),editable=False,default=0)
    task_root   = models.CharField(_("local task root"),max_length=255,validators=[validate_path],
                                   null=True,blank=True)
    task_parent = models.CharField(_("local task parent"),max_length=255,validators=[validate_path],
                                   null=True,blank=True)
    type_flags  = models.IntegerField(_("flags"),default=0)
    weight      = models.IntegerField(_("weigth"),default=0)
    objects     = MenuViewManager()
    
    class Meta(Resource.Meta):
        verbose_name        = _('menu view')
        verbose_name_plural = _('menu views')
        unique_together     = (('site','name'),('site','path'))
        app_label           = 'sitemenu'
        permissions         = (("call_menuview","Can call menu view"),)

    @property
    def path_pattern(self):
        if not hasattr(self,'_path_pattern_cache'):
            if self.view_args:
                args  = list(self.view_args)
                parts = map(lambda x: x if x != '%' else args.pop(0),
                            PathValidator.RE_SPLIT.split(self.path))
                self._path_pattern_cache = '/'.join(parts) + "$"
            else:
                self._path_pattern_cache = self.path + "$"
        return self._path_pattern_cache

    @property
    def path_regex(self):
        if not hasattr(self,'_path_regex_cache'):
            self._path_regex_cache = re.compile(self.path_pattern)
        return self._path_regex_cache

    @property
    def callback(self):
        if not hasattr(self,'_view_func_cache'):
            self._view_func_cache = get_callable(self.view_func)
        return self._view_func_cache

    def __unicode__(self):
        return self.path

    def save(self,*args,**kwargs):
        if self.name:
            pk_list = self._default_manger.filter(name__exact=self.name).value_list('id',flat=True)
            if pk_list and pk_list[0] != self.pk:
                raise IntegrityError,"Duplicate name '{0}'".format(self.name)
        parts            = PathValidator.RE_SPLIT.split(self.path)
        self.parts_count = len(parts)
        self.parts_fit   = 0
        slashes          = len(parts)-1
        argn             = 0
        for i in xrange(self.parts_count):
            match = PathValidator.RE_PART.match(parts[i])
            if not match:
                raise ValueError,"Invalid path: {0}".format(self.path)
            match = match.groupdict()
            if match['part']:
                self.parts_fit |= 1 << (slashes - i)
            elif match['var']:
                if argn >= len(self.view_args):
                    raise ValidationError,_("Path part {0} must be defined in view arguments").format(index)
                else:
                    argn += 1
        super(MenuView,self).save(*args,**kwargs)
        if hasattr(self,'_path_pattern_cache'): delattr(self,'_path_pattern_cache')
        if hasattr(self,'_path_regex_cache'):   delattr(self,'_path_regex_cache')
        if hasattr(self,'_view_func_cache'):    delattr(self,'_view_func_cache')

signals.post_save.connect(MenuViewManager._update_cache,sender=MenuView)

